/**
 * Copyright (c) 2011 Tomi Paananen.
 *
 * Part of Quick Gestures engine library.
 */

// Own header
#include "gestureprocessor.h"

// qgengine includes
#include "qgenginepriv.h"
#include "qgutils.h"
#include "qgtrace.h"


using namespace QGEngine;


/*!
  \class GestureProcessor
  \brief Provides methods for processing the points of a gesture.
*/


/*!
  Constructor.
*/
GestureProcessor::GestureProcessor()
{
}


/*!
  Destructor.
*/
GestureProcessor::~GestureProcessor()
{
}


/*!
  Returns the top-left point of the imaginary rectangle bounding the pattern
  formed by \a points.
*/
QPoint GestureProcessor::topLeft(const PointList &points) const
{
    int minX(0), minY(0), maxX(0), maxY(0);

    // Get the minimum values on both axis.
    QGUtils::getMinMaxXY(points, minX, minY, maxX, maxY);

    QPoint topLeftPoint(minX, minY);
    DEBUG_INFO("[" << topLeftPoint.x() << "," << topLeftPoint.y() << "]");
    return topLeftPoint;
}


/*!
  Returns the center point of the imaginary rectangle bounding the pattern
  formed by \a points.
*/
QPoint GestureProcessor::center(const PointList &points) const
{
    int minX(0), minY(0), maxX(0), maxY(0);

    // Get the minimum and maximum values on both axis.
    QGUtils::getMinMaxXY(points, minX, minY, maxX, maxY);

    QPoint centerPoint((maxX + minX) / 2, (maxY + minY) / 2);
    DEBUG_INFO("[" << centerPoint.x() << "," << centerPoint.y() << "]");
    return centerPoint;
}


/*!
  Moves the set of points contained by \a points according to \a newTopLeft
  which is expected to contain the new top-left corner of the imaginary
  rectangle bounding the set of points.
*/
void GestureProcessor::moveTopLeft(PointList &points,
                                   const QPoint &newTopLeft /* = 0 */) const
{
    QPoint oldTopLeft(topLeft(points));

    const int moveX(oldTopLeft.x() - newTopLeft.x());
    const int moveY(oldTopLeft.y() - newTopLeft.y());

    const int pointCount(points.count());

    for (int i = 0; i < pointCount; ++i) {
        if (points[i] == BreakPoint) {
            continue;
        }

        points[i].rx() -= moveX;
        points[i].ry() -= moveY;
    }
}


/*!
  Moves the set of points contained by \a points according to \a newCenter
  which is expected to contain the new center point of the imaginary rectangle
  bounding the set of points.
*/
void GestureProcessor::moveCenter(PointList &points,
                                  const QPoint &newCenter) const
{
    int minX(0), minY(0), maxX(0), maxY(0);

    // Get the minimum values on both axis.
    QGUtils::getMinMaxXY(points, minX, minY, maxX, maxY);

    moveTopLeft(points, QPoint(newCenter.x() - qAbs(minX - maxX) / 2,
                               newCenter.y() - qAbs(minY - maxY) / 2));
}


/*!
  Scales the points contained by \a points according to \a scaleFactor which
  is expected to contain the new maximum distance of points on one axis i.e.
  the maximum length of the longer side of an imaginary rectangle bounding the
  set of points.
*/
void GestureProcessor::scale(PointList &points, quint32 scaleFactor) const
{
    // Find the maximum distance between points on a single axis (either
    // x or y axis, pick the one that contains the greater distance
    // between points.
    int minX(0), minY(0), maxX(0), maxY(0);

    QGUtils::getMinMaxXY(points, minX, minY, maxX, maxY);

    int maxDistX = qAbs(maxX - minX);
    int maxDistY = qAbs(maxY - minY);
    int maxDist = maxDistX;

    if (maxDistY > maxDist) {
        maxDist = maxDistY;
    }

    // Calculate the scale ratio.
    qreal scaleRatio = (qreal)scaleFactor / (qreal)maxDist;

    DEBUG_INFO("Calculated scale ratio is" << scaleRatio);

    const int pointCount(points.count());

    QPoint currentPoint;

    int newX(0), newY(0);

    for (int i = 0; i < pointCount; ++i) {
        currentPoint = points[i];

        if (currentPoint == BreakPoint) {
            continue;
        }

        newX = (int)qRound((qreal)currentPoint.x() * scaleRatio);
        newY = (int)qRound((qreal)currentPoint.y() * scaleRatio);

        points[i].setX(newX);
        points[i].setY(newY);
    }
}


/*!
  Distributes the set of points contained by \a points evenly. The new total
  number of points is determined by \a wantedPointCount. Returns the modified
  point list.
*/
PointList GestureProcessor::distributePointsEvenly(const PointList &points,
                                                   int wantedPointCount) const
{
    const int pointCount(points.count());

    if (pointCount < 2 || wantedPointCount < 3) {
        // Invalid argument; There cannot possibly be a reason redistribute the
        // points of a pattern that has less than three points.
        return points;
    }

    // Add the first point.
    PointList evenedPoints;
    evenedPoints.append(points[0]);
    int pointsAdded(1);

    // Get the total length of the lines formed by the points of the original
    // pattern and divide evenly according to the wanted point count.
    const qreal totLength(totalLength(points));
    const qreal partLength(totLength / (wantedPointCount - 1));

    DEBUG_INFO("Total length:" << totLength << "part length:" << partLength);

    LineData line;
    QPointF currentPoint(points[0]);
    QPointF lastPoint;
    int pointIndex(0);
    int oldPointIndex(-1);
    qreal travelledDistance(0);
    qreal leftOverDistance(0);

    while (pointsAdded < wantedPointCount && pointIndex < pointCount) {
        if (oldPointIndex != pointIndex) {
            // Calculate the line equation.
            // TODO CHECK!!!
            if (pointIndex + 1 < pointCount && points[pointIndex + 1] != BreakPoint) {
                line = lineData(points[pointIndex], points[pointIndex + 1]);
            }
            else {
                // The next point in the original point vector is a break
                // point.
                DEBUG_INFO("Break point found in index" << pointIndex + 1);

                // Calculate the left over distance i.e. the distance missing
                // from reaching the whole part length.
                leftOverDistance = partLength - travelledDistance;

                // Jump to the point after the break point.
                pointIndex += 2;

                if (pointIndex < pointCount) {
                    // We still original points left.
                    currentPoint = points[pointIndex];
                    evenedPoints.append(BreakPoint);
                    evenedPoints.append(points[pointIndex]);

                    // The break points are not included in the wanted point
                    // count.
                    ++pointsAdded;
                }

                travelledDistance = 0;
                continue;
            }

            currentPoint.setX(line.mFrom.x());
            currentPoint.setY(line.mFrom.y());
            oldPointIndex = pointIndex;
        } // if (oldPointIndex != pointIndex)

        if (line.mDirection == LineData::Undefined) {
            // Not a valid line!
            ++pointIndex;
            continue;
        }

        lastPoint = currentPoint;

        // Advance on the current line.
        if (line.mIsVerticalLine) {
            if (line.mDirection == LineData::Up) {
                currentPoint.ry() -= LineTrackingPrecision;
            }
            else {
                currentPoint.ry() += LineTrackingPrecision;
            }

            currentPoint.setX(line.mX);
        }
        else {
            // Not a vertical line.
            if (line.mDirection == LineData::Right) {
                currentPoint.rx() += LineTrackingPrecision;
            }
            else {
                currentPoint.rx() -= LineTrackingPrecision;
            }

            // y = ax + b
            currentPoint.setY(line.mSlope * currentPoint.x() + line.mB);
        }

        if ((currentPoint.y() < line.mTo.y() &&
             line.mDirection == LineData::Up) ||
            (currentPoint.x() > line.mTo.x() &&
             line.mDirection == LineData::Right) ||
            (currentPoint.y() > line.mTo.y() &&
             line.mDirection == LineData::Down) ||
            (currentPoint.x() < line.mTo.x() &&
             line.mDirection == LineData::Left)) {
            // The current line has been exceeded.
            ++pointIndex;
        }

        // Calculate the travelled distance.
        travelledDistance +=
            QGUtils::euclideanDistance(lastPoint, currentPoint);

        if (travelledDistance >= partLength + leftOverDistance) {
            // Time to add a point.
            QPoint newPoint(qRound(currentPoint.x()),
                            qRound(currentPoint.y()));

            if (evenedPoints[pointsAdded - 1] != newPoint) {
                evenedPoints.append(newPoint);
                ++pointsAdded;
            }

            travelledDistance = 0;
            leftOverDistance = 0;
        }
    }

    DEBUG_INFO("Done. The new vector has" << evenedPoints.count() <<
               "points (of which" << breakPointCount(evenedPoints) <<
               "are break points) and length of" << totalLength(evenedPoints) <<
               "units.");
    return evenedPoints;
}


/*!
  Mirros \a points on the axis determined by \a operateOnXAxis.
*/
void GestureProcessor::mirror(PointList &points,
                              bool operateOnXAxis) const
{
    DEBUG_INFO("Operating on X axis:" << operateOnXAxis);

    QPoint centerPoint(center(points));
    const int pointCount(points.count());
    QPoint point;

    for (int i = 0; i < pointCount; ++i) {
        point = points[i];

        if (point == BreakPoint) {
            continue;
        }

        if (operateOnXAxis) {
            points[i].setX(2 * centerPoint.x() - point.x());
        }
        else {
            points[i].setY(2 * centerPoint.y() - point.y());
        }
    }
}


/*!
  Swaps the X and Y coordinates of \a points.
*/
void GestureProcessor::swapXY(PointList &points) const
{
    int x;
    const int pointCount(points.count());
    QPoint point;

    for (int i = 0; i < pointCount; ++i) {
        point = points[i];

        if (point == BreakPoint) {
            continue;
        }

        x = point.x();
        points[i].setX(point.y());
        points[i].setY(x);
    }
}


/*!
  Resolves and returns a list of lines formed by the set of point pairs of
  \a points.
*/
QList<QLine> GestureProcessor::pointsToLines(const PointList &points) const
{
    const int pointCount(points.count());
    QList<QLine> lines;

    for (int i = 0; i < pointCount - 1; ++i) {
        if (points[i] == BreakPoint || points[i + 1] == BreakPoint) {
            continue;
        }

        lines.append(QLine(points[i], points[i + 1]));
    }

    return lines;
}


/*!
  Normalizes \a line.
*/
void GestureProcessor::normalizeLine(QLine &line) const
{
    QPoint min((line.x1() < line.x2()) ? line.x1() : line.x2(),
               (line.y1() < line.y2()) ? line.y1() : line.y2());

    line.setLine(line.x1() - min.x(), line.y1() - min.y(),
                 line.x2() - min.x(), line.y2() - min.y());
}


/*!
  Compares \a line1 and \a line2 and returns the difference.
*/
qreal GestureProcessor::compareLines(const QLine &line1,
                                     const QLine &line2) const
{
    return qMin(QGUtils::euclideanDistance(line1.p1(), line2.p1()) +
                QGUtils::euclideanDistance(line1.p2(), line2.p2()),
                QGUtils::euclideanDistance(line1.p1(), line2.p2()) +
                QGUtils::euclideanDistance(line1.p2(), line2.p1()));
}


/*!
  Calculates and returns the total length of the imaginary lines formed by
  \a points.
*/
qreal GestureProcessor::totalLength(const PointList &points) const
{
    qreal length(0);
    const int count(points.count());

    if (count < 2) {
        DEBUG_INFO("Less than two points in the given list!");
        return 0;
    }

    for (int i = 0; i < count - 1; ++i) {
        if (points[i] != BreakPoint &&
            points[i + 1] != BreakPoint) {
            length += QGUtils::euclideanDistance(points[i], points[i + 1]);
        }
    }

    return length;
}


/*!
  Calculates and returns the line data for a line between \a point1 and
  \a point2.

  Note that the order in which the points are given does affect the result.
*/
LineData GestureProcessor::lineData(const QPoint &point1,
                                    const QPoint &point2) const
{
    LineData data;
    data.mFrom = point1;
    data.mTo = point2;
    data.mLength = QGUtils::euclideanDistance(point1, point2);

    if (point1.x() - point2.x() == 0) {
        // The line is vertical and therefore, has no slope.
        data.mIsVerticalLine = true;
        data.mX = point1.x();

        if (point1.y() < point2.y()) {
            data.mDirection = LineData::Down;
        }
        else if (point1.y() > point2.y()) {
            data.mDirection = LineData::Up;
        }
        else {
            DEBUG_INFO("The two given points are the same!");
            data.mDirection = LineData::Undefined;
        }
    }
    else {
        // Not a vertical line.
        data.mIsVerticalLine = false;

        if (point1.x() < point2.x()) {
            data.mDirection = LineData::Right;
        }
        else {
            data.mDirection = LineData::Left;
        }

        // Calculate the slope: delta y / delta x.
        data.mSlope = (qreal)(point2.y() - point1.y()) /
                      (qreal)(point2.x() - point1.x());

        // (y - y1) = a(x - x1), where a is slope
        // => y = ax - ax1 + y1, where -ax1 + y1 = b
        // => b = y1 - ax1.
        data.mB = point1.y() - data.mSlope * (qreal)point1.x();
    }

    return data;
}


/*!
  Calculates and returns the number of break points in the set of points
  contained by \a points.
*/
int GestureProcessor::breakPointCount(const PointList &points) const
{
    QPoint point;
    int breakPoints(0);

    foreach (point, points) {
        if (point == BreakPoint) {
            ++breakPoints;
        }
    }

    return breakPoints;
}


// End of file.
